/**
 * Copyright (C) 2006 OQube 
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *  
 * author: Arnaud Bailly - abailly AT oqube DOT com
 * created: 15 juin 2006
 */
package speculoos.jndi.mappers;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;

import org.apache.commons.collections.MapUtils;

import speculoos.core.Mapper;
import speculoos.core.MapperException;
import speculoos.utils.VariableString;

/**
 * A mapper that handle creation of modification items from variables and their
 * environment. This class uses a LDAP-Java map and sets of operations expressed
 * in Java namespace to generate a complete set of
 * {@see javax.naming.directory.ModificationItem} items. It allows finer control
 * over the modification process than
 * {@see speculoos.jndi.mappers.ModificationsFromAttributes}.
 *  * <p>
 * By convention, if the content of an operation tag is the empty string,
 * <em>after</em> environment variables subsitution, this is considered an
 * <strong>empty value</strong> which may means different semantics according
 * the operation required:
 * <ul>
 * <li>if a replace operation ({@link javax.naming.directory.DirContext#REPLACE_ATTRIBUTE}),
 * an empty attribute value means that all values in the attribute are deleted,
 * leading eventually to deletion of attribute if it must hold some value, </li>
 * <li>if an add operation ({@link javax.naming.directory.DirContext#ADD_ATTRIBUTE}),
 * an exception will be thrown,</li>
 * <li>if a remove operation ({@link javax.naming.directory.DirContext#REMOVE_ATTRIBUTE}),
 * an empty attribute value means that the attribute itseld is deleted.</li>
 * </ul>
 * </p>
 * <p>
 * The various operations related methods
 * {@link #modifyAttribute(String, VariableString)},
 * {@link #addAttribute(String, VariableString)} and
 * {@link #deleteAttribute(String, VariableString)} may be called in any order
 * but this order will be preserved in the backend query. For example, if one
 * does the following sequence of operations: <code>
 * ModifyMapperImpl mmi = ...
 * mmi.modifyAttribute("toto",new VariableString("tutu"));
 * mmi.addAttribute("titi",new VariableString("titi"));
 * mmi.modifyAttribute("toto",new VariableString("tata"));
 * </code>
 * then a modify attribute on
 * <code>toto</toto> with values <code>["tutu","tata"]</code>
 * will be created and executed before an add operation on attribute <code>titi</code>.
 * </p>
 * 
 * @author nono
 * 
 */
public class ModificationsVariables implements Mapper {

	private Map operations = new LinkedHashMap();

	private Map outputMap;

	/*
	 * Class to use as key
	 */
	public static class AttributeAndOperation {

		String attr;

		int id;

		public AttributeAndOperation(String s, int id) {
			this.attr = s;
			this.id = id;
		}

		public boolean equals(Object obj) {
			AttributeAndOperation ao = (AttributeAndOperation) obj;
			if (ao == null)
				return false;
			return (ao.attr == null ? attr == null : ao.attr.equals(attr))
					&& (ao.id == id);
		}

		public int hashCode() {
			return attr.hashCode() ^ id;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#toString()
		 */
		public String toString() {
			switch (id) {
			case 0:
				return "modify " + attr;
			case 1:
				return "add " + attr;
			case 2:
				return "remove " + attr;
			default:
				return "";
			}
		}

	}

	/**
	 *  Construct this modification mapper with given 
	 *  output map.
	 *  Note that this map is inverted to use.
	 *  
	 * @param output
	 */
	public ModificationsVariables(Map output) {
		this.outputMap = MapUtils.invertMap(output);
	}

	/**
	 * Ask this mapper to modify given attribute. The attribute name is given
	 * relative to the object name space (ie. the input map), the output map is
	 * used to map this name to name of attribute in backend namespace.
	 * 
	 * @param string
	 *            attribute name in object namespace.
	 * @param vs
	 *            attribute value to use
	 */
	public void modifyAttribute(String string, VariableString vs) {
		operation(new AttributeAndOperation(string,
				DirContext.REPLACE_ATTRIBUTE), vs);
	}

	/**
	 * Ask this mapper to add some value to the given attribute. The attribute
	 * name is given relative to the object name space (ie. the input map), the
	 * output map is used to map this name to name of attribute in backend
	 * namespace.
	 * 
	 * @param string
	 *            attribute name in object namespace.
	 * @param vs
	 *            attribute value to use. Will be instantiated add execution
	 *            time.
	 */
	public void addAttribute(String string, VariableString vs) {
		operation(new AttributeAndOperation(string, DirContext.ADD_ATTRIBUTE),
				vs);
	}

	/**
	 * Ask this mapper to delete some value to the given attribute. The
	 * attribute name is given relative to the object name space (ie. the input
	 * map), the output map is used to map this name to name of attribute in
	 * backend namespace.
	 * 
	 * @param string
	 *            attribute name in object namespace.
	 * @param vs
	 *            attribute value to use. Will be instantiated add execution
	 *            time.
	 */
	public void deleteAttribute(String string, VariableString vs) {
		operation(
				new AttributeAndOperation(string, DirContext.REMOVE_ATTRIBUTE),
				vs);
	}

	/**
	 * Adds a new operation to this modify mapper.
	 * 
	 * @param ao
	 *            an object containing attribute name and operation code.
	 * @param vs
	 *            variable string denoting value of attribute.
	 */
	public void operation(AttributeAndOperation ao, VariableString vs) {
		List l = (List) operations.get(ao);
		if (l == null) {
			operations.put(ao, l = new ArrayList());
		}
		l.add(vs);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.core.Mapper#map(java.lang.Object, java.util.Map)
	 */
	public Object map(Object input, Map param) throws MapperException {
		ModificationItem[] mods = new ModificationItem[operations.size()];
		int k = 0;
		for (Iterator i = operations.entrySet().iterator(); i.hasNext(); k++) {
			Map.Entry e = (Map.Entry) i.next();
			AttributeAndOperation ao = (AttributeAndOperation) e.getKey();
			List l = (List) e.getValue();
			/* find item in output map */
			String aname = (String) outputMap.get(ao.attr);
			if (aname == null)
				throw new MapperException(getName() + " : attribute " + ao.attr
						+ " is not mapped to directory");
			/* create attribute */
			BasicAttribute ba = new BasicAttribute(aname);
			for (Iterator j = l.iterator(); j.hasNext();) {
				String val = ((VariableString) j.next()).instance(param);
				if (val != null && !"".equals(val))
					ba.add(val);
			}
			/* create mod item */
			mods[k] = new ModificationItem(ao.id, ba);
		}
		return mods;

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.core.Mapper#getName()
	 */
	public String getName() {
		return "ModificationsVariables";
	}

	/**
	 * @return Returns the outputMap.
	 */
	public Map getOutputMap() {
		return outputMap;
	}

	/**
	 * Make the output map used for this filter as the inverse of the given map.
	 * <strong>NOTE</strong>: standard maps have LDAP names as key and Java
	 * names as values.
	 * 
	 * @param outputMap
	 *            The outputMap to set. It must be bijective and not contain
	 *            null keys or values.
	 */
	public void setOutputMap(Map outputMap) {
		this.outputMap = MapUtils.invertMap(outputMap);
	}

}
